﻿using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Text.RegularExpressions;
using Fuse8.ContentManagementFramework.Domain;
using Fuse8.ContentManagementFramework.SystemFacade.Contents;
using Fuse8.DomainFramework.Common.Locator;
using Fuse8.DomainFramework.DataAccess;
using Fuse8.DomainFramework.DataAccess.SqlDatabase;
using Fuse8.DomainFramework.Common;

namespace Fuse8.ContentManagementFramework.BizFacade.Services.Contents.Queryable
{
    public class CmsContentQueryBuilder
    {
        private Regex joinAlias = new Regex("Id$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
        private string whereClause;
        private int countToTake;
        private int countToSkip;
        private string orderByClause;
        private string selectClause = "it.*";
        private string joinClause = "";
        private bool orderDescending;
        private bool selectOnlyCount;
        public int ContentTypeId { get; protected set; }
        public bool IgnoreRevisionsSlicer { get; set; }
        public List<object> Parameters { get; private set; }


        public CmsContentQueryBuilder(int contentTypeId, bool ignoreRevisionsSlicer = false)
        {
            ContentTypeId = contentTypeId;
            IgnoreRevisionsSlicer = ignoreRevisionsSlicer;
            Parameters = new List<object>();
        }

        public void SetWhere(Expression expression)
        {
            InvocationExpression right = Expression.Invoke(expression, Slicer.Parameters);
            expression = Expression.Lambda(Expression.And(Slicer.Body, right), Slicer.Parameters);
            whereClause = new WhereVisitor(Parameters).GetSqlQuery(expression);
        }

        private Expression<Func<GenericCmsEntity, bool>> Slicer
        {
            get
            {
                var context = DependencyServiceLocator.Current.GetInstance<ICmsContext>();
                int siteId = context.ContextSite.Id;
                int groupId = context.ContextSite.SiteGroupId;
                Expression<Func<GenericCmsEntity, bool>> slicer =
                    p =>
                    ((p.PropertyIsNull("SiteGroupId") && p.PropertyIsNull("SiteId")) ||
                     (p.PropertyEquals("SiteGroupId", groupId) && p.PropertyIsNull("SiteId") ||
                      (p.PropertyEquals("SiteGroupId", groupId) && p.PropertyEquals("SiteId", siteId))));

                slicer = slicer.And(p => p.PropertyEquals("IsDeleted", false));
                if (!context.IsInAdministrationArea)
                {
                    var date = context.CurrentDateTime;
                    slicer =
                        slicer.And(
                            p =>
                                (p.PropertyIsNull("PublishStartDate") ||
                                 p.PropertyLessOrEquals("PublishStartDate", date)) &&
                                (p.PropertyIsNull("PublishEndDate") || p.PropertyGreaterOrEquals("PublishEndDate", date)));
                }
                if (!IgnoreRevisionsSlicer)
                {
                    if (context.IsInAdministrationArea)
                    {
                        slicer = slicer.And(p => p.PropertyEquals("IsLatestRevision", true));
                    }
                    else
                    {
                        slicer = slicer.And(p => p.PropertyEquals("IsLatestCompletedRevision", true));
                    }
                }
                return slicer;
            }
        }

        public void SetTake(Expression expression)
        {
            var exp = expression as ConstantExpression;
            countToTake = Convert.ToInt32(exp.Value);
        }

        public void SetOrder(Expression expression, bool desc)
        {
            orderByClause = new OrderByVisitor().GetSqlClause(expression);
            orderDescending = desc;
        }

        public void SetSkip(Expression expression)
        {
            var exp = expression as ConstantExpression;
            countToSkip = Convert.ToInt32(exp.Value);
        }

        public string BuildQuery()
        {
            var sb = new StringBuilder();
            sb.Append("SELECT");
            if (!selectOnlyCount)
            {
                if (countToTake > 0)
                {
                    sb.AppendFormat(" TOP {0}", countToTake);
                }
                sb.AppendFormat(" {0}", selectClause);
            }
            else
            {
                sb.Append(" COUNT(it.Id)");
            }

            if (string.IsNullOrEmpty(whereClause))
            {
                EnsureSlicer();
            }

            sb.AppendFormat(" FROM {0} it", GetSource());            
            if (!string.IsNullOrEmpty(joinClause))
            {
                sb.Append(joinClause);
            }
            
            if (!string.IsNullOrEmpty(whereClause))
            {                
                sb.AppendFormat(" WHERE {0}", whereClause);
            }

            if (!string.IsNullOrEmpty(orderByClause) && !selectOnlyCount)
            {
                sb.AppendFormat(" ORDER BY {0}", orderByClause);
                if (orderDescending)
                {
                    sb.Append(" DESC");
                }
            }

            return sb.ToString();
        }

        private void EnsureSlicer()
        {
            whereClause = new WhereVisitor(Parameters).GetSqlQuery(Slicer);
        }

        private string GetSource()
        {
            string result;
            if (countToSkip == 0)
            {
                result = DependencyServiceLocator.Current.GetInstance<IContentSchemeBuilder>().GetDataEntityName(ContentTypeId);
            }
            else
            {
                string orderBy = "order by Id";
                if (!string.IsNullOrEmpty(orderByClause))
                {
                    orderBy = "order by " + orderByClause.Replace("it.", "it1.");
                    if (orderDescending)
                    {
                        orderBy += " desc";
                        orderDescending = false;
                    }
                    orderByClause = "it.[ContentRowNumber]";
                }
                result = "(SELECT it1.*, row_number() over("+orderBy+") as ContentRowNumber FROM " +
                         DependencyServiceLocator.Current.GetInstance<IContentSchemeBuilder>().GetDataEntityName(
                             ContentTypeId) + " it1";
                if (!string.IsNullOrEmpty(whereClause))
                {
                    result += " WHERE " + whereClause.Replace("it.", "it1.");
                }
                result += ")";

                whereClause = string.Format("it.ContentRowNumber > {0}", countToSkip);
            }
            return result;

        }

        public void SelectCount(bool selectCount)
        {
            selectOnlyCount = selectCount;
        }

        public void SetJoinContent(Expression expression)
        {
            var exp = expression as MethodCallExpression;
            if (exp == null)
            {
                throw new NotSupportedException("This method supports only MethodCallExpression");
            }

            int typeId = Convert.ToInt32(((ConstantExpression)exp.Arguments[1]).Value);
            string primaryField = ((ConstantExpression) exp.Arguments[2]).Value.ToString();
            string foreignField = ((ConstantExpression)exp.Arguments[3]).Value.ToString();
            joinClause += string.Format(" LEFT JOIN {0} as it{1} on it.[{2}] = it{1}.[{3}]",
                                        DependencyServiceLocator.Current.GetInstance<IContentSchemeBuilder>().
                                            GetDataEntityName(typeId), typeId, foreignField, primaryField);

            var addOn = new WhereVisitor(Parameters).GetSqlQuery(Slicer);
            addOn = addOn.Replace("it.", string.Format("it{0}.", typeId));
            joinClause += " AND " + addOn;

            var fields = new SelectVisitor().ParseForSelects(exp.Arguments[4]);
            if (fields.Count > 0)
            {
                foreach (var field in fields)
                {
                    selectClause += string.Format(", it{0}.[{1}] as [{2}_{1}]", typeId, field,
                                                  joinAlias.Replace(foreignField, ""));
                }                
            }
        }

        public void SetJoinWith(Expression expression)
        {
            var exp = expression as MethodCallExpression;
            if (exp == null)
            {
                throw new NotSupportedException("This method supports only MethodCallExpression");
            }
            Type entityType = exp.Method.IsGenericMethod
                                  ? exp.Method.GetGenericArguments()[0]
                                  : ((ConstantExpression) exp.Arguments[4]).GetValueFromConst<Type>();

            string foreignField = ((ConstantExpression)exp.Arguments[2]).Value.ToString();
            string primaryKey = new JoinKeyVisitor().GetFieldName(exp.Arguments[1]);
            string tableName = SqlEntityHelper.GetEntityTableName(entityType);
            string sqlPrimaryKey =
                SchemaBuilder.Current.GetColumns(entityType, GetSqlConnection(entityType)).FirstOrDefault(
                    p => p.EntityPropertyName == primaryKey).SqlColumnName;
            joinClause += string.Format(" LEFT JOIN {0} as {0}{1} on it.[{2}] = {0}{1}.[{3}]", tableName, joinAlias.Replace(foreignField, ""), foreignField, sqlPrimaryKey);

            var fields = new SelectVisitor().ParseForSelects(exp.Arguments[3]);
            if (fields.Count > 0)
            {
                foreach (var field in fields)
                {
                    selectClause += string.Format(", {0}{1}.[{2}] as [{1}_{2}]", tableName, joinAlias.Replace(foreignField, ""), field);
                }
            }
        }

        private SqlConnection GetSqlConnection(Type entityType)
        {
            // just to flag any issues with signatures changing define function here
            Func<SqlConnection> exp =
                () =>
                (DependencyServiceLocator.Current.GetInstance<IUnitOfWorkFactory<CmsUser>>().Create(false) as
                 SqlUnitOfWork<CmsUser>).Connection;

            var factoryType = typeof (IUnitOfWorkFactory<>).MakeGenericType(entityType);
            var factory = DependencyServiceLocator.Current.IsRegistered(factoryType) ? DependencyServiceLocator.Current.GetInstance(factoryType):null;
            if (factory == null)
            {
                return null;
            }

            var method = factory.GetType().GetMethod("Create");
            if (method != null)
            {
                var unitOfWork = method.Invoke(factory, new object[] {false});
                var property = unitOfWork.GetType().GetProperty("Connection");
                if (property != null)
                {
                    return property.GetValue(unitOfWork, null) as SqlConnection;
                }
            }

            return null;
        }

        public void SetSelect(Expression expression)
        {
            var fields = new SelectVisitor().ParseForSelects(expression);
            if (fields.Count > 0)
            {
                selectClause = "";
                foreach (var field in fields)
                {
                    selectClause += string.Format("it.[{0}], ", field);
                }

                selectClause = selectClause.Substring(0, selectClause.LastIndexOf(","));
            }
        }
    }
}